// Copyright 2007 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.enterprise.connector.afyd;

import com.google.enterprise.connector.spi.RepositoryException;


import com.google.gdata.data.DateTime;
import com.google.gdata.data.Entry;
import com.google.gdata.data.Feed;
import com.google.gdata.client.Query;
import com.google.gdata.client.calendar.CalendarService;
import com.google.gdata.util.AuthenticationException;
import com.google.gdata.util.ServiceException;
import com.google.gdata.util.NotModifiedException;

import java.net.URL;
import java.net.MalformedURLException;
import java.io.IOException;
import java.util.List;
import java.util.LinkedList;
import java.util.logging.Logger;
import java.util.Comparator;
import java.util.Collections;


/**
 * This class fills the EntryFeedProvider role for the case of the hosted 
 * Calendar service.  A GData CalendarService is needed to power this class.
 * 
 * @author amsmith@google.com (Adam Smith)
 */
public class CalendarEntryProvider  implements FeedEntryProvider {
  
  
  /** The logger for this class. */
  private static final Logger LOGGER =
      Logger.getLogger(CalendarEntryProvider.class.getName());
  
  /**
   * The maximum number of entries to be fetched when requesting new items
   * from the feed.  If more than this number of results are available,
   * last-updated-time traversal order is NOT guaranteed and some updates
   * can be missed.
   */
  private static final int MAX_RESULTS = Integer.MAX_VALUE;
  
  /** The service that will actually be used to fetch feed entries. */
  private CalendarService service;
  
  private String domain;
  
  private Comparator comparator;
  
  private String lastEntryId;
  private String lastEntryCheckpoint;
  
  public static final String EVENT_FEED_PATTERN =
    "https://www.google.com/calendar/feeds/{userName}@{domain}/private/basic";
  
  /**
   * Constructs a CalendarEntryProvider using the given service for fetching
   * feed entries.
   * @param calendarService the back end service in question
   * @param email is the domain admin's email
   * @param password is the domain admin's password
   */
  public CalendarEntryProvider
      (CalendarService calendarService, String email, String password)
      throws AuthenticationException {

    calendarService.setUserCredentials(email, password);
    this.service = calendarService;
    this.domain = email.substring(email.indexOf("@") + 1);
    this.comparator = new TraversalOrderComparator();
  }
  
  /**
   * {@inheritDoc}
   * 
   * The chosen traversal order for this feed provider is "by id alphabetically,
   * by last modified time".
   */
  public List getOrderedEntriesForUser(String username, String checkpoint) 
  throws RepositoryException {
    String urlString = EVENT_FEED_PATTERN
                        .replaceAll("\\{userName\\}", username)
                        .replaceAll("\\{domain\\}", domain);
    URL feedUrl = null;
    try {
      feedUrl = new URL(urlString); 
    } catch (MalformedURLException murle) {
      LOGGER.severe(murle.toString());
      throw new RepositoryException(murle);
    }
    
    DateTime ifModifiedSince = null;
    if (checkpoint != null) {
      String dateString = checkpoint.substring(0, checkpoint.indexOf("!"));
      try {
        ifModifiedSince = DateTime.parseDateTime(dateString);
      } catch (NumberFormatException nfe) {
        LOGGER.info("Got " + nfe.toString() + " while parsing date part of"
            + "checkpoint.  Continuing as if no date was specified.");
      }
    }
    // If ifModifiedSince is null at this point service.query() will treat it
    // as if no threshold was specified (equivalent to the 2-arg form).
    
    DateTime fetchTime = DateTime.now();
    Query query = new Query(feedUrl);
    query.setMaxResults(MAX_RESULTS);
    if (ifModifiedSince != null) {
      // The use of ifModifiedSince here filters out entries that were
      // modified before the given date.  Logically, we only care about those
      // entries that were modified recently.
      query.setUpdatedMin(ifModifiedSince);
    }
    
    Feed feed = null;
    try {
      feed = (Feed) service.query(query, Feed.class, ifModifiedSince);
    } catch (NotModifiedException nme) {
      // excellent! no work to do
      return new LinkedList();
    } catch (ServiceException se) {
      LOGGER.severe(se.toString());
      throw new RepositoryException(se);
    } catch (IOException ioe) {
      LOGGER.severe(ioe.toString());
      throw new RepositoryException(ioe);
    }
    List entries = feed.getEntries();
    Collections.sort(entries, comparator);
    Entry lastEntry = (Entry) entries.get(entries.size() - 1);
    lastEntryId = lastEntry.getId();
    lastEntryCheckpoint = fetchTime.toString() + "!" + lastEntryId;
    
    
    if (checkpoint != null && ifModifiedSince != null) {
      // All of these entries were modified during or after the checkpoint time
      // but we still need to skip over any that have been processed before
      // during the checkpoint time.
      String idString = checkpoint.substring(checkpoint.indexOf("!") + 1);
      while (entries.size() > 0) {
        Entry firstEntry = (Entry) entries.get(0);
        boolean isIdLessEq = 
          firstEntry.getId().compareTo(idString) <= 0;
        boolean isUpdatedEqual = 
          firstEntry.getUpdated().compareTo(ifModifiedSince) == 0;
        if (isIdLessEq && isUpdatedEqual) {
          entries.remove(0);
        } else {
          break;
        }
      }
    }
    
    return entries;
  }
  
  /**
   * {@inheritDoc}
   * 
   * The checkpoint method for this feed provider is "LASTMOD_TIME!ID".
   */
  public String getCheckpointForEntry(Entry entry) {
    if (lastEntryId != null && lastEntryId.equals(entry.getId())) {
      return lastEntryCheckpoint;
    } else {
      return entry.getUpdated().toString() + "!" + entry.getId();
    }
  }

  /**
   * This class implements Comparator for the traversal order described above.
   */
  public static class TraversalOrderComparator implements Comparator {
    
    public int compare(Object o1, Object o2) {
      Entry e1 = (Entry) o1;
      Entry e2 = (Entry) o2;
      
      int updatedResult = e1.getUpdated().compareTo(e2.getUpdated());
      if (updatedResult == 0) {
        return e1.getId().compareTo(e2.getId());
      } else {
        return updatedResult;
      }
    }
    
    public boolean equals(Object o1, Object o2) {
      Entry e1 = (Entry) o1;
      Entry e2 = (Entry) o2;
      return e1.getId().compareTo(e1.getId()) == 0;
    }
  }
  
}
